/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ArkFile, AstTreeUtils, ts} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData} from "../BaseChecker";
import {FileMatcher, MatcherCallback, MatcherTypes, MethodMatcher} from "../../matcher/Matchers";
import {Defects, IssueReport} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {RuleListUtil} from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoUnsafeCallCheck');
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/no-unsafe-call.md", // TODO: support url
    description: "Disallow calling a function with a value with type `any`"
};

export class NoUnsafeCallCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatchBuildCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        }
        return [fileMatchBuildCb];
    }

    public check = (arkFile: ArkFile) => {
        if (!arkFile.getFilePath().endsWith(".ts")) {
            return;
        }
        const code = arkFile.getCode();
        if (!code) {
            return;
        }
        const asRoot = AstTreeUtils.getASTNode(arkFile.getName(), code);
        const sourceFileObject = ts.getParseTreeNode(asRoot);
        if (sourceFileObject == undefined) {
            return;
        }
        this.loopNode(arkFile, asRoot, sourceFileObject);
    }

    public loopNode(targetFile: ArkFile, sourceFile: ts.SourceFile, aNode: ts.Node) {
        const children = aNode.getChildren();
        for (const child of children) {
            if (ts.isFunctionDeclaration(child)) {
                this.checkFunctionDeclaration(child, sourceFile, targetFile);
            }
            if (ts.isExpressionStatement(child)) {
                this.checkExpressionStatement(child, sourceFile, targetFile);
            }
            this.loopNode(targetFile, sourceFile, child);
        }
    }

    private checkFunctionDeclaration(child: ts.FunctionDeclaration, sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        function getLastExpression(expression: ts.Expression): ts.Expression {
            if (ts.isPropertyAccessExpression(expression)) {
                return getLastExpression(expression.expression);
            } else {
                return expression;
            }
        }

        if (child.body && ts.isBlock(child.body)) {
            child.body!.statements.forEach(statement => {
                if (ts.isExpressionStatement(statement)) {
                    if (ts.isCallExpression(statement.expression)) {
                        if (ts.isIdentifier(statement.expression.expression)) {
                            let positionInfo = this.getPositionInfo(statement.expression.expression, sourceFile);
                            let name = statement.expression.expression.getText();
                            this.checkParameter(child, name, positionInfo, arkFile);
                        }
                        if (ts.isPropertyAccessExpression(statement.expression.expression)) {
                            let positionInfo = this.getPositionInfo(statement.expression.expression, sourceFile);
                            let className = getLastExpression(statement.expression.expression)!.getText();
                            let name = statement.expression.expression.name.getText();
                            this.checkPropertyAccessParameter(child, className, name, positionInfo, arkFile);
                        }
                    }
                    if (ts.isNewExpression(statement.expression)) {
                        if (ts.isIdentifier(statement.expression.expression)) {
                            let positionInfo = this.getPositionInfo(statement.expression, sourceFile);
                            let name = statement.expression.expression.getText();
                            this.checkParameter(child, name, positionInfo, arkFile);
                        }
                        if (ts.isPropertyAccessExpression(statement.expression.expression)) {
                            let positionInfo = this.getPositionInfo(statement.expression, sourceFile);
                            let name = statement.expression.expression.name.getText();
                            const className = ts.isIdentifier(statement.expression.expression.expression) ? statement.expression.expression.expression.getText() : '';
                            this.checkPropertyAccessParameter(child, className, name, positionInfo, arkFile);
                        }
                    }
                    if (ts.isTaggedTemplateExpression(statement.expression)) {
                        if (ts.isIdentifier(statement.expression.tag)) {
                            let positionInfo = this.getPositionInfo(statement.expression.tag, sourceFile);
                            let name = statement.expression.tag.getText();
                            this.checkParameter(child, name, positionInfo, arkFile);
                        }
                        if (ts.isPropertyAccessExpression(statement.expression.tag)) {
                            let positionInfo = this.getPositionInfo(statement.expression.tag, sourceFile);
                            let name = statement.expression.tag.name.getText();
                            const className = ts.isPropertyAccessExpression(statement.expression.tag) ? statement.expression.tag.expression.getText() : '';
                            this.checkPropertyAccessParameter(child, className, name, positionInfo, arkFile);
                        }
                    }
                }
            })
        }
    }

    private checkParameter(child: ts.FunctionDeclaration, name: string, positionInfo: any, arkFile: ArkFile): void {
        child.parameters!.forEach(parameter => {
            if (ts.isParameter(parameter)) {
                if (parameter.name.getText() === name) {
                    if (parameter.type?.kind === ts.SyntaxKind.AnyKeyword) {
                        this.addAstIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                    }
                }
            }
        })
    }

    private checkPropertyAccessParameter(child: ts.FunctionDeclaration, className: any, name: string, positionInfo: any, arkFile: ArkFile): void {
        child.parameters!.forEach(parameter => {
            if (ts.isParameter(parameter)) {
                if (parameter.name.getText() === className) {
                    if (parameter.type?.kind === ts.SyntaxKind.AnyKeyword) {
                        this.addAstIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                    }
                }
            }
            if (parameter.type && ts.isTypeLiteralNode(parameter.type)) {
                parameter.type.members.forEach(member => {
                    if (ts.isPropertySignature(member)) {
                        if (member.name!.getText() === name) {
                            if (member.type?.kind === ts.SyntaxKind.AnyKeyword) {
                                this.addAstIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                            }
                        }
                    }
                })
            }
        })
    }


    private checkExpressionStatement(child: ts.ExpressionStatement, sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        function findLastParent(child: ts.Node): ts.Node | null {
            if (!child || !child.parent) {
                return null;
            }
            if (!child.parent.parent) {
                return child.parent;
            }
            return findLastParent(child.parent);
        }
        let positionInfo = this.getPositionInfo(child, sourceFile);
        if (child.expression) {
            let className: string = '';
            let name: string = '';
            if (ts.isTaggedTemplateExpression(child.expression)) {
                positionInfo = this.getPositionInfo(child.expression.tag, sourceFile);
                className = child.expression.tag.getText();
                name = ts.isPropertyAccessExpression(child.expression.tag) ? child.expression.tag.name.getText() : '';
            } else if (ts.isCallExpression(child.expression)) {
                positionInfo = this.getPositionInfo(child.expression.expression, sourceFile);
                className = this.getClassNameFromExpression(child.expression.expression);
                name = this.getNameFromExpression(child.expression.expression);
            } else if (ts.isNewExpression(child.expression)) {
                positionInfo = this.getPositionInfo(child.expression, sourceFile);
                className = this.getClassNameFromExpression(child.expression.expression);
                name = this.getNameFromExpression(child.expression.expression);
            }
            if (className) {
                let findLast = findLastParent(child);
                if (findLast) {
                    findLast.getChildren()[0].getChildren().forEach(node => {
                        if (ts.isVariableStatement(node)) {
                            node.declarationList.declarations.forEach(declaration => {
                                if (declaration.name.getText() === className) {
                                    this.checkDeclarationType(declaration, positionInfo, arkFile);
                                }
                                if (declaration.type && ts.isTypeLiteralNode(declaration.type)) {
                                    this.checkTypeLiteralMembers(declaration.type, name, positionInfo, arkFile);
                                }
                            });
                        }
                    });
                }
            }
        }
    }

    private getClassNameFromExpression(expression: ts.Expression): string {
        if (ts.isIdentifier(expression)) {
            return expression.getText();
        } else if (ts.isPropertyAccessExpression(expression)) {
            return this.getLastExpression(expression).getText();
        } else if (ts.isElementAccessExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
            return expression.expression.expression.getText();
        }
        return '';
    }

    private getNameFromExpression(expression: ts.Expression): string {
        if (ts.isPropertyAccessExpression(expression)) {
            return expression.name.getText();
        } else if (ts.isElementAccessExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
            return expression.expression.name.getText();
        }
        return '';
    }

    private getLastExpression(expression: ts.Expression): ts.Expression {
        if (ts.isPropertyAccessExpression(expression)) {
            return this.getLastExpression(expression.expression);
        } else {
            return expression;
        }
    }

    private checkDeclarationType(declaration: ts.VariableDeclaration, positionInfo: any, arkFile: ArkFile): void {
        if (declaration.type?.kind === ts.SyntaxKind.AnyKeyword) {
            this.addAstIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
        }
    }

    private checkTypeLiteralMembers(type: ts.TypeLiteralNode, name: string, positionInfo: any, arkFile: ArkFile): void {
        type.members.forEach(member => {
            if (ts.isPropertySignature(member) && member.name!.getText() === name) {
                if (member.type?.kind === ts.SyntaxKind.AnyKeyword) {
                    this.addAstIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                }
            }
        });
    }

    private getPositionInfo(expression: ts.Node, sourceFile: ts.SourceFileLike) {
        const start = expression.getStart();
        const end = expression.getEnd();
        const startPositionInfo = sourceFile.getLineAndCharacterOfPosition(start);
        const endPositionInfo = sourceFile.getLineAndCharacterOfPosition(end);
        return {
            startPosition: startPositionInfo,
            endPosition: endPositionInfo
        };
    }

    private addAstIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string) {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const defect = new Defects(line, startCol, endCol, message, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.defects.push(defect);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}